package cbbx_sm.probabilistic_model;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.jgrapht.WeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;

import cbbx_sm.parser.BoundingBox;
import cbbx_sm.parser.CameraData;
import cbbx_sm.parser.Entity;
import cbbx_sm.parser.Frame;
import cbbx_sm.utils.BoundingBoxKMeansClustering;
import cbbx_sm.utils.DefaultWeightedEdgeComparator;
import cbbx_sm.utils.ExperimentManager;
import cbbx_sm.utils.Utility;


public class Clustering {
	public static List<Cluster> kMeansClustering(CameraData cameraData, int k){
		List<BoundingBox> boxes = new ArrayList<BoundingBox>();
		for (Frame f : cameraData.getFrames()) {
			for(Entity e : f.getEntities()) {
				boxes.add(e.getBoxes()[ExperimentManager.boxLocationId]);				
			}
		}

		
		ArrayList<Cluster> clusters = new ArrayList<Cluster>();
		ArrayList<Object> inputBoxes = new ArrayList<Object>();
		for (BoundingBox box: boxes){
			inputBoxes.add(box);
		}
		BoundingBoxKMeansClustering bbc = new BoundingBoxKMeansClustering();
		ArrayList<Short> result = bbc.run(inputBoxes, k);
		for (int i=0; i<k; i++){
			clusters.add(new Cluster(cameraData.getCameraId()));
		}
		
		for (int i=0; i<inputBoxes.size(); i++){
			Cluster c = clusters.get(result.get(i));
			c.addElement((BoundingBox)inputBoxes.get(i));
		}
		int[] counts = new int[k];
		for (Short s: result){
			counts[s]++;
		}
		
		for (int i=0; i<counts.length; i++){
			System.out.println("cluster "+i+" count = "+counts[i]);
		}
		
		return clusters;

	} 
	
	/**
	 * A trivial clustering procedure...
	 * 
	 * @param boxes
	 * @return
	 */
	public static List<Cluster> trivialClustering(Set<BoundingBox> boxes){
		List<Cluster> clusters = new Vector<Cluster>();
		double threshold = 220.0; //220.0
		boolean added;
		
		for (BoundingBox b : boxes){
			added = false;
			
			for (Cluster c : clusters)
				if (c.getClusterDistance(b) <= threshold){
					c.addElement(b);
					added = true;
					break;
				}
			
			if (!added){
				// XXX: change name of clusters if you want to use them
				// with a CamaraData structure.
				Cluster c = new Cluster("_none_");
				clusters.add(c);
				c.addElement(b);
			}
		}
		
		return clusters;
	}
	
	/**
	 * Perform a variation of the kruskal clustering algorithm.
	 * 
	 * @param g - graph
	 * @return clusters
	 */
	public static List<Cluster> kruskalClustering(WeightedGraph<BoundingBox, DefaultWeightedEdge> g, double threshold){
		List<Cluster> clusters = new Vector<Cluster>();
		
		// Creating the initial set of clusters: one per vertex
		for (BoundingBox b : g.vertexSet()){
			// XXX: change name of clusters if you want to use them
			// with a CamaraData structure.
			Cluster current = new Cluster("_none_");
			current.addElement(b);
			clusters.add(current);
		}
		
		// Ordering the graph edges
		List<DefaultWeightedEdge> edges = orderEdgesByWeight(g);
		
		DefaultWeightedEdge currEdge;

		do{
			if (edges.size() == 0) break;
			
			// Getting and removing the smallest edge of the graph
			currEdge = edges.get(0);
			if (g.getEdgeWeight(currEdge) > threshold) break;
//			edges.remove(0);
			
			// Getting source vertex and target vertex of that edge
			BoundingBox source = g.getEdgeSource(currEdge);
			BoundingBox target = g.getEdgeTarget(currEdge);
						
			// Finding owining clusters of the vertexes of this edge
			Cluster sourceVertexCluster = getOwningCluster(clusters, source);
			Cluster targetVertexCluster = getOwningCluster(clusters, target);
			
			// (Not necessary with reduction, i.e.,) if I delete all
			// edges that links vertexes in the same cluster...
			if (sourceVertexCluster.contains(target)) continue; 
						
			// Fusing clusters linked by this edge:
			// removing old clusters and adding the new fused one
			Cluster fusedCluster = fuseClusters(sourceVertexCluster, targetVertexCluster);
			clusters.remove(sourceVertexCluster);
			clusters.remove(targetVertexCluster);
			clusters.add(fusedCluster);
			
			// Recomputing edges weight and reducing edges... OLD VERSION
//			edges = recomputeEdges(edges, clusters, g, source, target);
			
			// Add and delete this two vertexes, so I delete all
			// their edges...
			g.removeVertex(source);
			g.removeVertex(target);
			g.addVertex(source);
			
			for (BoundingBox b : g.vertexSet()){
				if (b == source) continue;
				DefaultWeightedEdge e = g.addEdge(source, b);
				g.setEdgeWeight(e, fusedCluster.getClusterDistance(b));
			}
			
			edges = orderEdgesByWeight(g);
		}
		while(true);
		
		return clusters;
	}
	
	public static List<Cluster> kruskalClustering(Set<BoundingBox> boxes, double threshold){
		return kruskalClustering(Utility.buildBBoxesGraph(boxes), threshold);
	}

	/**
	 * Given a list of clusters and a set of bounding boxes, this method will update
	 * clusters adding all the boxes to owning cluster. It is useful for calculating
	 * radius of a cluster given its coordinate and the original training set of 
	 * bounding boxes.
	 * 
	 * @param clusters
	 * @param boxes
	 */
	public static void _updateClustersForRadius(List<Cluster> clusters, Set<BoundingBox> boxes){
		// Removing from clusters old elements...
		for (Cluster cluster : clusters)
			for (int i = 0; i < cluster.size(); i++)
				cluster.removeElementWithoutUpdatingCenter(cluster.getElement(i));
		
		for (BoundingBox box : boxes)
			getFutureOwningCluster(clusters, box).addElementWithoutUpdatingCenter(box);
	}
	
	/**
	 * Order edges in a graph based on their weight.
	 * @param g
	 * @return
	 */
	public static List<DefaultWeightedEdge> orderEdgesByWeight(WeightedGraph<BoundingBox, DefaultWeightedEdge> g){
		List<DefaultWeightedEdge> orderedList = new Vector<DefaultWeightedEdge>(g.edgeSet());
		Collections.sort(orderedList, new DefaultWeightedEdgeComparator(g));
		
		return orderedList;
	}
	
	/**
	 * Given a list of clusters and a bounding box, returns the cluster that
	 * actually contains that bounding box.
	 * 
	 * @param clusters
	 * @param box
	 * @return container cluster
	 */
	private static Cluster getOwningCluster(List<Cluster> clusters, BoundingBox box){
		if (box == null || clusters == null) return null;
		for (Cluster c : clusters)
			if (c.contains(box)) return c;
		return null;
	}
	
	/**
	 * Given a list of clusters and a bounding box, returns the cluster that 
	 * should contain that bounding box.
	 * 
	 * @param clusters
	 * @param box
	 * @return container cluster
	 */
	public static Cluster getFutureOwningCluster(List<Cluster> clusters, BoundingBox box){
		Cluster cluster = null;
		double distance = Double.POSITIVE_INFINITY;
		
		for (Cluster c : clusters){
			double d = c.getClusterDistance(box);
			if (d < distance) {
				distance = d;
				cluster = c;
			}
		}
		
		return cluster;
	}
	
	
	/**
	 * Given two clusters, this method returns a new cluster that contains
	 * all the elements of both cluster 1 and cluster 2 (parameters).
	 * 
	 * @param c1
	 * @param c2
	 * @return new cluster derived from c1 and c2
	 */
	private static Cluster fuseClusters(Cluster c1, Cluster c2){
		// XXX: change name of clusters if you want to use them
		// with a CamaraData structure.
		Cluster fusionCluster = new Cluster("_none_");
		
		for (int i = 0; i < c1.size(); i++)
			fusionCluster.addElement(c1.getElement(i));
		for (int i = 0; i < c2.size(); i++)
			fusionCluster.addElement(c2.getElement(i));
		
		return fusionCluster;
	}
	
	/*
	 * Ad-hoc parsing for clusters description files
	 */
	public static List<Cluster> parseClusters(String filename, String cameraId) throws IOException{
		List<Cluster> clusters = new Vector<Cluster>();
		BufferedReader in = new BufferedReader(
				new InputStreamReader(
				new FileInputStream(filename)));
		
		String line;
		while((line = in.readLine()) != null){
			if (line.charAt(0) == '*') continue;
			Cluster cluster = new Cluster(cameraId);
			Double x = Double.parseDouble(line.split(";")[0].split("\\(")[1]);
			Double y = Double.parseDouble(line.split(";")[1].split("\\)")[0]);
			Double radius = Double.parseDouble(line.split("RADIUS: ")[1]);
			
			cluster.setX(x);
			cluster.setY(y);
			cluster.setRadius(radius);
			
			clusters.add(cluster);
		}
		
		return clusters;
	}
}
